3.5. 调试汇编代码

除了高级 C 和 C++ 调试之外,GDB 还可以在汇编代码级别调试程序。这样做使 GDB 能够列出函数中的反汇编代码序列,在汇编指令级别设置断点,一次逐步执行一条汇编指令,并在运行时检查存储在机器寄存器以及栈和堆内存地址中的值。我们在本节中使用 IA32 作为示例汇编语言,但此处介绍的 GDB 命令适用于 GCC 支持的任何汇编语言。我们注意到,在阅读了后面章节中有关汇编代码的更多信息后,读者可能会发现本小节最有用。

我们使用以下简短的 C 程序作为示例:

int main(void) {
    int x, y;

    x = 1;
    x = x + 2;
    x = x - 14;
    y = x * 100;
    x = x + y * 6;

    return 0;
}

要编译为 IA32 可执行文件,请使用 -m32 标志:

$ gcc -m32 -o simpleops simpleops.c

或者,使用 gcc-fno-asynchronous-unwind-tables 命令行选项进行编译会生成 IA32 代码,程序员更容易阅读和理解:

$ gcc -m32 -fno-asynchronous-unwind-tables -o simpleops simpleops.c

3.5.1. 使用 GDB 检查二进制代码

在本节中,我们将展示一些在汇编代码级别调试短 C 程序的示例 GDB 命令。下表总结了本节演示的许多命令:

GDB commandDescription
break sumSet a breakpoint at the beginning of the function sum
break *0x0804851aSet a breakpoint at memory address 0x0804851a
disass mainDisassemble the main function
niExecute the next instruction
siStep into a function call (step instruction)
info registersList the register contents
p $eaxPrint the value stored in register %eax
p *(int *)($ebp+8)Print out the value of an int at an address (%ebp+8)
x/d $ebp+8Examine the contents of memory at an address

首先,编译为 IA32 汇编指令集集并在 IA32 架构的可执行程序 simpleops 上运行 GDB:

$ gcc -m32 -fno-asynchronous-unwind-tables -o simpleops simpleops.c
$ gdb ./simpleops

然后,在 main 中设置断点,然后使用 run 命令开始运行程序:

(gdb) break main
(gdb) run

disass 命令反汇编(列出与之关联的汇编代码)程序的各个部分。例如查看main函数的汇编指令:

(gdb) disass main         # Disassemble the main function

GDB 允许程序员通过取消引用指令的内存地址来在各个汇编指令处设置断点:

(gdb) break *0x080483c1   # Set breakpoint at instruction at 0x080483c1

程序的执行可以一次执行一条汇编指令,使用 sini 进入或执行下一条指令:

(gdb) ni     # Execute the next instruction

(gdb) si     # Execute next instruction; if it is a call instruction,
             # then step into the function

si 命令进入函数调用,这意味着 GDB 将在被调用函数的第一条指令处暂停程序。 ni 命令会跳过它们,这意味着 GDB 将在调用指令之后的下一条指令处暂停程序(在函数执行并返回到调用者之后)。

程序员可以使用 print 命令和前缀为 $ 的寄存器名称来打印存储在机器寄存器中的值:

(gdb) print $eax    # print the value stored in register eax

display 命令在到达断点时自动显示值:

(gdb) display $eax
(gdb) display $edx

info registers 命令显示存储在机器寄存器中的所有值:

(gdb) info registers

3.5.2. 使用 DDD 在程序集级别进行调试

DDD 调试器在另一个调试器(本例中为 GDB)之上提供了一个图形界面。它提供了一个很好的界面,用于显示汇编代码、查看寄存器和单步执行 IA32 指令。由于 DDD 有单独的窗口来显示反汇编代码、寄存器值和 GDB 命令提示符,因此在汇编代码级别进行调试时,它通常比 GDB 更容易使用。

要使用 DDD 进行调试,请将 ddd 替换为 gdb

$ ddd ./simpleops

GDB 提示符出现在底部窗口中,它在提示符处接受 GDB 命令。虽然它为一些 GDB 命令提供了菜单选项和按钮,但通常底部的 GDB 提示符更容易使用。

DDD 通过选择 ViewMachine Code Window 菜单选项来显示程序的汇编代码视图。该选项创建一个新的子窗口,其中包含程序的汇编代码列表(您可能需要调整此窗口的大小以使其更大)。

要在单独的窗口中查看程序的所有寄存器值,请启用 StatusRegisters 菜单选项。

3.5.3. GDB汇编代码调试命令和示例

以下是一些有助于在汇编代码级别进行调试的 GDB 命令的详细信息和示例(请参阅常用 GDB 命令 部分,了解有关其中一些命令的更多详细信息,特别是 printx 格式选项):

  • disass: 反汇编一个函数或某些地址范围的代码。
    disass <func_name>   # Lists assembly code for function
    disass <start> <end> # Lists assembly instructions between start & end address
    
    disass main          # Disassemble main function
    disass 0x1234 0x1248 # Disassemble instructions between addr 0x1234 & 0x1248
  • break: 在指令地址处设置断点。
    break *0x80dbef10  # Sets breakpoint at the instruction at address 0x80dbef10
  • stepi (si), nexti (ni) :
    stepi, si          # Execute next machine code instruction,
                       # stepping into function call if it is a call instr
    nexti,  ni         # Execute next machine code instruction,
                       # treating function call as a single instruction
  • info registers: 列出所有寄存器值。

  • print: 显示表达式的值。

    print $eax                # Print the value stored in the eax register
    print *(int *)0x8ff4bc10  # Print int value stored at memory addr 0x8ff4bc10
  • x 显示给定地址的内存位置的内容。请记住, x 的格式是粘性的(sticky),因此需要显式更改。
    (gdb) x $ebp-4      # Examine memory at address: (contents of register ebp)-4
                        # if the location stores an address x/a, an int x/wd, ...
    
    (gdb) x/s 0x40062d  # Examine the memory location 0x40062d as a string
    0x40062d   "Hello There"
    
    (gdb) x/4c 0x40062d # Examine the first 4 char memory locations
                        # starting at address 0x40062d
    0x40062d   72 'H'  101 'e' 108 'l' 108 'l'
    
    (gdb) x/d 0x40062d  # Examine the memory location 0x40062d in decimal
    0x40062d   72       # NOTE: units is 1 byte, set by previous x/4c command
    
    (gdb) x/wd 0x400000 # Examine memory location 0x400000 as 4 bytes in decimal
    0x400000   100      # NOTE: units was 1 byte set, need to reset to w
  • set: 设置内存位置和寄存器的内容。
    set $eax = 10                 Set the value of register eax to 10
    set $esp = $esp + 4           Pop a 4-byte value off the stack
    set *(int *)0x8ff4bc10 = 44   Store 44 at address 0x8ff4bc10
  • display: 每次命中断点时打印一个表达式。
    display $eax         Display value of register eax

3.5.4. 汇编调试常用命令快速汇总

$ ddd ./a.out
(gdb) break main
(gdb) run

(gdb) disass main         # Disassemble the main function
(gdb) break sum           # Set a breakpoint at the beginning of a function
(gdb) cont                # Continue execution of the program
(gdb) break *0x0804851a   # Set a breakpoint at memory address 0x0804851a
(gdb) ni                  # Execute the next instruction
(gdb) si                  # Step into a function call (step instruction)
(gdb) info registers      # List the register contents
(gdb) p $eax              # Print the value stored in register %eax
(gdb) p  *(int *)($ebp+8) # Print out value of an int at addr (%ebp+8)
(gdb) x/d $ebp+8          # Examine the contents of memory at the given
                          #  address (/d: prints the value as an int)
(gdb) x/s 0x0800004       # Examine contents of memory at address as a string
(gdb) x/wd 0xff5634       # After x/s, the unit size is 1 byte, so if want
                          # to examine as an int specify both the width w & d